This chapter discusses in detail the Collection objects included in the Containers Library. It describes how to use the collections and the different types of collections available.
In this chapter you will learn:
- What are collections?
- How to use the collections
- The characteristics of each type of collection
What are collections?
A collection is generic data structure that stores pointers to data items. That is, when an item is inserted in the collection, the collection stores only the address at which the item is located in memory. When the item is retrieved from the collection, the collection returns the memory address by which the data item can be referenced. A collection is a more general concept than the traditional array, set or list. As with all containers, collections are polymorphic, which means that they can store any type of data. They are also dynamically sized, as most containers. With a collection, you set an initial size, but it can grow at run time to accommodate the data stored in it.
The basic collection object by default assumes that data items in the collection are TObject descendants. Sorted collections are collections of items sorted by a key and string collections are collections that assume that data items in the collection are dynamically allocated strings. Both sorted and unsorted string collections are included in the library.
The Containers Library implements several new collections that complement Borland's TCollection object. These new collections allow you to create huge collections or choose whether to store the collection's data in conventional memory, in expanded memory or even in a file on disk. Additional methods in the containers' programming interface also provide improved access to data stored in a collection.
All collections require the use of dynamically allocated data.
Huge collections
Huge collections in the Containers Library complement Borland's TCollection object, by providing an object that has no practical limit in the number of items it can store. This section describes the process of using a THugeCollection object in your applications. However, the topics discussed in here are not restricted to THugeCollection but are valid also for all the other collections included in the library.
Creating a collection
As when using any other container in the library, the first thing you need to do to use a collection is to create the data type you wish to store in it. In our examples, we'll use the following object definition:
type
PContact = ^TContact;
TContact = object(TObject)
FirstName,
LastName,
Phone,
Company : PString;
constructor Init(ALastName, AFirstName, APhone,
ACompany: string);
destructor Done; virtual;
end;
Next, you must implement TContact's Init and Done methods, to allocate and dispose of the data in the object.
Note that TContact.Done will be called automatically for each TContact object, when you dispose of the entire collection.
Using the collection
Now you are ready to use the collection. To start using a collection in your application, all you need to do is create an instance of the collection and start inserting items into it. For example:
The DisplayContacts procedure shows an example of how to use a ForEach iterator, one of the several iterator methods supported by containers. ForEach takes one parameter to a procedure, which must be a local procedure declared using the far model (either by using the far directive or the $F+ compiler directive). This procedure takes one parameter, which is a pointer to an item stored in the container.
Another kind of iterator is the FirstThat method. It will return the first item in the container that matches a condition. Instead of taking a pointer to a far local procedure, it takes as a parameter a pointer to a far local boolean function:
procedure FindLastName(ContactList: PSequence;
LastName: string);
var
Item : Pointer;
Index : LongInt;
function MatchLastName(Item: Pointer): Boolean; far;
Writeln('Item found with last name ''', LastName, ''':');
with PContact(Item)^ do
Writeln(LastName^, '':15 - Length(LastName^),
FirstName^, '':15 - Length(FirstName^), Phone^,
'':20 - Length(Phone^), Company^, '':20 -
Length(Company^));
{ not required }
ContactList^.DoneItem(Item);
end;
FirstThat also takes an Index parameter, in which it will store the index of the item returned. This value can be used as a seed value in other methods, as for instance, the NextThat method. If used after a call to FirstThat, NextThat will return the second item in the container that satisfies a given condition:
Notice the way the DoneItem method is used in the last two examples, and in the DisplayFirst and DisplayLast methods:
procedure DisplayFirst(ContactList: PSequence);
var
Item : Pointer;
Index : LongInt;
begin
Item := ContactList^.First(Index);
{...}
{ not required }
ContactList^.DoneItem(Item);
end;
procedure DisplayLast(ContactList: PSequence);
var
Item : Pointer;
Index : LongInt;
begin
Item := ContactList^.Last(Index);
{...}
{ not required }
ContactList^.DoneItem(Item);
end;
DoneItem is always called after the item retrieved from the container is no longer needed. Although collections keep all data in memory always and therefore, the use of DoneItem is not required, if you want to make your code data-structure independent you should always use DoneItem. In the above examples, if you decide you don't want to use a collection anymore, but use a stream based container instead, you won't have to modify any of the code in the examples. Depending on the container that you choose, all you will have to do is initialize the new object instead of the collection and recompile your program! Note, however, that if the new container does not support the data type you are using, you will also need to change the type definition of your data.
Sorted collections
A sorted collection is a collection that lets you store items sorted by key. For using a sorted collection, the first thing you need to do is teach the collection how to sort the items. For this purpose, you need to override the KeyOf method and in some cases, the Compare method also.
For creating a sorted list of contacts in our previous example, the first thing you must do is override the KeyOf method of the THugeSortedCollection object, as shown here:
Is that simple! These are usually also the same steps that you will follow when using any other sorted container in the library, like the sorted arrays, sorted linked lists, B trees, etc.
String collections
String collections are a special type of collection that stores pointers to dynamically allocated strings. Using a string collection is easy: simply create an instance of the string collection you want to use, and then start adding items:
Notice that when you dispose the collection, it will take care of deallocating all the strings for you. Therefore, you don't have to worry about manually disposing of individual strings.
Using Search
In sorted sequences, you can use the Search method to find an item with a given key. In the case of string collections, the key of an item in the collection is the item itself:
procedure FindString(StringList: PSequence;
SearchStr : string);
var
Index : LongInt;
begin
if StringList^.Search(@SearchStr, Index)
then begin
Writeln('String found:');
Writeln(PString(StringList^.At(Index))^)
end
else Writeln('String not found.');
end;
Unsorted string collections
You can also create unsorted string collections by using the THugeUnSortedStrCollection object (see collect4.pas). For searching for an item in an unsorted string collection, you can use one of the conditional iterative methods supported by the containers, like for example, the FirstThat method:
Calling the Search method in unsorted containers will result in a "Call to abstract method" error.
Stream collections
Stream collections are another type of collection, in which the data in the collection is stored in a stream, rather than in the heap. For example, you can store the collection in expanded memory and save conventional memory for other uses. Note, however, that stream collections store in the stream only the pointers to data items. The data itself is always kept in the heap.
Using a stream collection is as easy as using a normal collection. All you have to do is create an instance of the stream collection that you want to use:
var
ContactInfo : PEmsCollection;
begin { collect5.pas }
{...}
ContactInfo := New(PEmsCollection, Init(50, 10));
if ContactInfo = nil
then begin
Writeln('No EMS memory available.');
Halt(1);
end;
{...}
Dispose(ContactInfo, Done);
end.
By default, stream collections use a TMemoryStream to store the data in the collection. If you wish to use a different type of stream, you must override the InitStream method. This is what the TEmsXXXX collections do.